﻿using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Linq.Expressions;

using IQToolkit;

namespace LINQToolKit.Query
{
    public class EnumerableExecutor : IEnumerableExecutor
    {
        internal sealed class ExpressionMethodsVisitor : ExpressionVisitor
        {
            private IList<MethodCallExpression> _methods;

            public ExpressionMethodsVisitor()
            {
                this._methods = new List<MethodCallExpression>();
            }

            public IList<MethodCallExpression> LookupMethods(Expression expression)
            {
                this._methods.Clear();
                this.Visit(expression);

                return this._methods;
            }

            protected override Expression VisitMethodCall(MethodCallExpression m)
            {
                if (m.Method.DeclaringType == typeof(Queryable))
                {
                    this._methods.Add(m);
                }

                return base.VisitMethodCall(m);
            }
        }

        public object Execute(Expression expr, IEnumerable source)
        {
            expr.AssertNull("expr");
            source.AssertNull("source");
            var visitor = new ExpressionMethodsVisitor();
            var methods = visitor.LookupMethods(expr).Reverse();
            object result = source;

            foreach (var m in methods)
            {
                result = this.ExecuteMethod(m, result);
            }

            return result;
        }

        private object ExecuteMethod(MethodCallExpression m, object source)
        {
            var genericArgTypes = m.Method.GetGenericArguments();
            var parameters =
                (from p in m.Method.GetParameters()
                 select p.ParameterType).ToArray<Type>();
            var parameterValues = new List<object>();
            parameterValues.Add(source);

            for (int i = 1; i < m.Arguments.Count; i++)
            {
                if (m.Arguments[i].NodeType == ExpressionType.Constant)
                {
                    parameterValues.Add(((ConstantExpression)m.Arguments[i]).Value);
                }
                else
                {
                    var lambda = (LambdaExpression)((UnaryExpression)m.Arguments[i]).Operand;
                    parameterValues.Add(lambda.Compile());
                }
            }

            var method = GetEnumerableMethod(m.Method.Name, parameters);
            return method.MakeGenericMethod(genericArgTypes).Invoke(null, parameterValues.ToArray());
        }

        public static MethodInfo GetEnumerableMethod(string methodName, Type[] parameters)
        {
            var methods =
                from m in typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)
                where m.Name == methodName
                select m;

            foreach (var m in methods)
            {
                var methodParams =
                        (from p in m.GetParameters()
                         select p.ParameterType).ToArray<Type>();

                if (m.GetParameters().Length == parameters.Length)
                {
                    return m;
                }
            }

            return null;
        }
    }
}
